Skip to content

fix: deallocate failed prepare by server-side statement name - #2646

Open
istoolsfox wants to merge 1 commit into
jackc:masterfrom
istoolsfox:fix/failed-prepare-deallocation-name
Open

fix: deallocate failed prepare by server-side statement name#2646
istoolsfox wants to merge 1 commit into
jackc:masterfrom
istoolsfox:fix/failed-prepare-deallocation-name

Conversation

@istoolsfox

Copy link
Copy Markdown

Problem

When a prepare fails after ParseComplete (e.g. a statement_timeout or pg_cancel_backend landing between Parse and Describe), the statement has already been created on the server under the digest name stmt_<sha256>. The deferred cleanup introduced in 5.8.0 for #2223 stores psKey — the SQL text — instead of psName, so Conn.Deallocate looks it up in c.preparedStatements, finds nothing (a failed prepare was never stored there), and falls through to sending the Close protocol message with the full SQL text as the statement name. No statement by that name exists, closing one is not an error, so the cleanup "succeeds" while deallocating nothing.

This is invisible through the pgx.Conn API (callers pass name != sql there), but it is the default path for stdlib.Conn.PrepareContext, which calls c.conn.Prepare(ctx, query, query) — as used by GORM with PrepareStmt: true. On such a connection the server-side stmt_<digest> leaks, and every retry of the same SQL fails with 42P05 duplicate_prepared_statement — which is itself a PrepareError, so failedDescribeStatement gets set again and the next cleanup no-ops again, poisoning the connection for its remaining lifetime. That is precisely the failure class the #2223 fix was meant to close, still reachable through the digest path.

Fix

Store psName (the name the server actually knows) instead of psKey:

if errors.As(err, &pErr) {
    c.failedDescribeStatement = psName
}

For the named path (name != sql), psName == psKey, so behavior there is unchanged. For the digest path, the deferred Deallocate now sends Close for stmt_<digest>, which removes the leaked statement and lets the retry succeed. If the failure happened before ParseComplete, no statement exists and the Close is a harmless no-op, as before.

Test

TestPrepareHandlesTimeoutBetweenParseAndDescribeWhenNameEqualsSQL mirrors the existing #2223 test but goes through the digest path (Prepare(ctx, sql, sql)), using the faultyconn hook to induce the same deterministic timeout between Parse and Describe. It verifies the statement exists on the server under its digest name after the failed prepare, and that a retry of the same SQL succeeds.

On the previous code the test fails exactly as reported:

ERROR: prepared statement "stmt_91c32deb..." already exists (SQLSTATE 42P05)

With the fix it passes. Full pgx root package and stdlib test suites pass against PostgreSQL 17.

Fixes #2640

When name == sql, Prepare creates the statement on the server under a
digest name (stmt_<sha256>) while the client maps it under the SQL
text. If the prepare failed after ParseComplete (e.g. statement_timeout
between Parse and Describe), failedDescribeStatement stored the SQL
text, so the deferred Deallocate sent Close with the SQL text as the
statement name. That closes nothing: the stmt_<sha256> statement leaks
and every retry of the same sql on this connection fails with 42P05
duplicate_prepared_statement, re-arming the failed cleanup each time
for the lifetime of the connection.

Store the server-side name (psName) instead. For the named path
(name != sql) psName and psKey are identical, so behavior there is
unchanged.

Fixes jackc#2640
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant